# -------------------------------------------------------------------------
#
#  Part of the CodeChecker project, under the Apache License v2.0 with
#  LLVM Exceptions. See LICENSE for license information.
#  SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
# -------------------------------------------------------------------------
""" Creates a Makefile from analyzer actions. """

import hashlib
import os
import shlex
import uuid

from codechecker_common.logger import get_logger

from codechecker_statistics_collector.collectors.special_return_value import \
    SpecialReturnValueCollector

from . import analysis_manager, env
from .analyzers.clangsa.analyzer import ClangSA
from .analyzers.clangsa.ctu_manager import generate_ast_cmd, \
    get_extdef_mapping_cmd
from .analyzers.clangsa.ctu_triple_arch import get_triple_arch
from .analyzers.clangsa.statistics import build_stat_coll_cmd
from .analyzers.clangtidy.analyzer import ClangTidy


LOG = get_logger('analyzer')


class MakeFileCreator:
    """ Creates a Makefile from analyzer actions. """

    def __init__(self, analyzers, output_path, config_map, context,
                 skip_handler, pre_analysis, statistics_data, ctu_data):
        self.__analyzers = analyzers
        self.__output_path = output_path
        self.__config_map = config_map
        self.__context = context
        self.__skip_handler = skip_handler
        self.__pre_analysis = pre_analysis
        self.__log_info = "[`date +'%Y-%m-%d %H:%M:%S'`] -"

        self.__ctu_data = ctu_data
        self.__ctu_dir = None
        self.__ctu_temp_fnmap_folder = None
        if ctu_data:
            self.__ctu_dir = ctu_data['ctu_dir'] if ctu_data else None
            self.__ctu_temp_fnmap_folder = ctu_data['ctu_temp_fnmap_folder']

        self.__statistics_data = statistics_data
        self.__stat_tmp_dir = None
        self.__stats_dir = None
        if statistics_data:
            self.__stat_tmp_dir = statistics_data['stat_tmp_dir']
            self.__stats_dir = statistics_data['stats_out_dir']

        self.__makefile = os.path.join(output_path, 'Makefile')

        self.__analyzer_env = env.extend(context.path_env_extra,
                                         context.ld_lib_path_extra)

        self.__config = None
        self.__func_map_cmd = None
        if ClangSA.ANALYZER_NAME in config_map:
            self.__config = config_map[ClangSA.ANALYZER_NAME]

            ctu_capability = config_map[ClangSA.ANALYZER_NAME].ctu_capability
            self.__func_map_cmd = ctu_capability.mapping_tool_path

    def __format_analyzer_type(self, analyzer_type):
        """ Format the given analyzer type. """
        return analyzer_type.replace('-', '')

    def __get_target_name(self, action):
        """ Get target name for the given action. """
        analyzer_name = self.__format_analyzer_type(action.analyzer_type)
        target_name = analyzer_name + '_' + action.source + '_' + \
            action.original_command
        return hashlib.md5(target_name.encode('utf-8')).hexdigest()

    def __write_header(self, mfile):
        """ Write header section to the given file.

        Write a header section to this file which tells the user that this
        file is auto generated by CodeChecker and print out the exact
        CodeChecker version.
        """
        mfile.write("#\n# Autogenerated by CodeChecker v{0}.\n#\n"
                    "# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT "
                    "YOU ARE DOING.\n#\n\n".format(self.__context.version))

    def __write_env_exports(self, mfile):
        """ Exports environment variables. """
        data_files_dir_path = os.getenv("CC_DATA_FILES_DIR")
        if data_files_dir_path:
            bin_dir = os.getenv('CC_BIN_DIR', '')
            python3_bin = os.path.join(data_files_dir_path, 'python3', 'bin')

            mfile.write('export PATH := {0}:{1}:${{PATH}}\n'.format(
                bin_dir, python3_bin))

        for env_var in ["LD_LIBRARY_PATH", "PYTHONPATH", "PYTHONHOME"]:
            value = os.getenv(env_var)
            if value:
                mfile.write('export {0} := {1}\n'.format(env_var, value))

        mfile.write('\n')

    def __write_default_targets(self, mfile):
        """ Write default targets to the given file.

        This will add 'all' target to be the default target and creates an
        'all_<analyzer_name>' target to run all analysis of the specified
        analyzer.
        """
        mfile.write("# Default target to run all analysis.\n"
                    "default: all\n\n")

        for analyzer in self.__analyzers:
            analyzer_name = self.__format_analyzer_type(analyzer)
            mfile.write("# Target to run only '{0}' analysis.\n"
                        "all: all_{0}\n\n".format(analyzer_name))

    def __get_ctu_pre_analysis_cmds(self, action):
        """ Get CTU pre-analysis commands. """
        cmds = []

        # Get architecture part of the target triple.
        triple_arch = get_triple_arch(action, action.source,
                                      self.__config, self.__analyzer_env)

        # Get command to generate PCH file.
        cmd, ast_dir = generate_ast_cmd(action, self.__config,
                                        triple_arch, action.source)
        cmds.append('mkdir -p {0}'.format(ast_dir))
        cmds.append(' '.join(cmd))

        # Get command to create CTU index file.
        cmd = get_extdef_mapping_cmd(action, self.__config,
                                     action.source, self.__func_map_cmd)

        fnmap_tmp_dir = os.path.join(self.__ctu_dir, triple_arch,
                                     self.__ctu_temp_fnmap_folder)
        cmds.append('mkdir -p {0}'.format(fnmap_tmp_dir))

        func_def_map = os.path.join(fnmap_tmp_dir, str(uuid.uuid4()))
        cmds.append('{0} > {1} 2>/dev/null'.format(' '.join(cmd),
                                                   func_def_map))

        # Modify externalDefMap.txt file to contain relative paths and
        # modify the extension to '.cpp.ast'.
        # The sed command is a bit different in Mac OS X, the ‘-i’ option
        # requires a parameter to tell what extension to add for the backup
        # file. For this reason we do not use this option, instead redirects
        # the sed output to a temp file and overwrite the original file with
        # this file.
        tmp_func_def_map = func_def_map + '_tmp'
        cmds.append('sed -E "s|/(.*)|ast/\\1.ast|" {0} > {1}'.format(
            func_def_map, tmp_func_def_map))
        cmds.append('mv -f {0} {1}'.format(tmp_func_def_map, func_def_map))

        return cmds

    def __get_stats_pre_analysis_cmds(self, action):
        """ Get statistics pre-analysis commands. """
        cmds = []

        stats_cmd, can_collect = build_stat_coll_cmd(action, self.__config,
                                                     action.source)
        if can_collect:
            cmds.append('mkdir -p ' + self.__stat_tmp_dir)
            _, source_filename = os.path.split(action.source)
            output_id = source_filename + str(uuid.uuid4()) + '.stat'

            stat_for_source = os.path.join(self.__stat_tmp_dir, output_id)
            cmds.append('{0} > {1} 2>&1'.format(' '.join(stats_cmd),
                                                stat_for_source))

        return cmds

    def __write_pre_analysis_targets(self, mfile, action, pre_all_target):
        """ Creates the pre-analysis targets. """
        pre_all_cmds = []

        # Get CTU pre-analysis commands.
        if self.__ctu_data:
            pre_all_cmds.extend(self.__get_ctu_pre_analysis_cmds(action))

        # Get statistics pre-analysis commands.
        if self.__statistics_data:
            pre_all_cmds.extend(self.__get_stats_pre_analysis_cmds(action))

        commands = '\n'.join(['\t@' + c for c in pre_all_cmds])

        target = self.__get_target_name(action)
        mfile.write('{0}:\n'
                    '\t@echo "{4} Pre-analysis of {3}."\n'
                    '{1}\n'
                    '{2}: {0}\n\n'.format('pre_' + target,
                                          commands,
                                          pre_all_target,
                                          action.source,
                                          self.__log_info))

    def __write_post_pre_analysis_targets(self, mfile, pre_all_target):
        """ Creates targets to post-process pre-analysis results. """
        # Get CTU pre-analysis commands.
        post_all_cmds = []

        if self.__ctu_data:
            # Merge individual function maps into a global one.
            post_all_cmds.append("find {0} -maxdepth 1 -mindepth 1 -type d "
                                 "-exec merge-clang-extdef-mappings "
                                 "-i {{}}/{1} -o {{}}/externalDefMap.txt "
                                 "\\;".format(
                                     self.__ctu_dir,
                                     self.__ctu_temp_fnmap_folder))

        if self.__statistics_data:
            # Collect statistics from the clang analyzer output.
            post_all_cmds.append("post-process-stats -i {0} {1}".format(
                                     self.__stat_tmp_dir,
                                     self.__stats_dir))

        commands = '\n'.join(['\t@' + c for c in post_all_cmds])

        mfile.write('post_{0}: {0}\n'
                    '{1}\n\n'.format(pre_all_target,
                                     commands))

    def __write_analysis_targets(self, mfile, action, post_pre_all_target):
        """ Creates normal analysis targets. """
        source_analyzer, rh = analysis_manager.prepare_check(
            action, self.__config_map.get(action.analyzer_type),
            self.__output_path, self.__context.checker_labels,
            self.__skip_handler, self.__statistics_data)

        if self.__statistics_data and post_pre_all_target:
            stats_cfg = SpecialReturnValueCollector.checker_analyze_cfg(
                self.__stats_dir)

            source_analyzer.add_checker_config(stats_cfg)

        analyzer_cmd = source_analyzer.construct_analyzer_cmd(rh)

        # Escape elements before join theme into one string.
        analyzer_cmd = map(shlex.quote, analyzer_cmd)

        target = self.__get_target_name(action)
        analyzer_name = self.__format_analyzer_type(action.analyzer_type)

        if action.analyzer_type == ClangTidy.ANALYZER_NAME:
            analyzer_output_file = rh.analyzer_result_file + ".output"
            file_name = "{source_file}_{analyzer}_" + target
            report_converter_cmd = ["report-converter",
                                    "-t", "clang-tidy",
                                    "-o", self.__output_path,
                                    "--filename", file_name,
                                    analyzer_output_file]

            command = "@{0} > {1}\n" \
                      "\t@{2} 1>/dev/null\n" \
                      "\t@rm -rf {1}\n".format(' '.join(analyzer_cmd),
                                               analyzer_output_file,
                                               ' '.join(report_converter_cmd))
        else:
            command = "@{0} 1>/dev/null".format(' '.join(analyzer_cmd))

        mfile.write('{0}: {1}\n'
                    '\t@echo "{6} {4} analyze {5}."\n'
                    '\t{2}\n'
                    'all_{3}: {0}\n\n'.format(target,
                                              post_pre_all_target,
                                              command,
                                              analyzer_name,
                                              action.analyzer_type,
                                              action.source,
                                              self.__log_info))

    def create(self, actions):
        """ Creates a Makefile from the given actions. """
        LOG.info("Creating Makefile from the analyzer commands: '%s'...",
                 self.__makefile)

        with open(self.__makefile, 'w+',
                  encoding='utf-8', errors='ignore') as mfile:
            self.__write_header(mfile)
            self.__write_env_exports(mfile)
            self.__write_default_targets(mfile)

            clangsa_analyzer_name = \
                self.__format_analyzer_type(ClangSA.ANALYZER_NAME)
            pre_all_target = 'pre_all_' + clangsa_analyzer_name

            for action in actions:
                need_pre_analysis_targets = self.__pre_analysis and \
                    action.analyzer_type == ClangSA.ANALYZER_NAME

                post_pre_all_target = ''
                if need_pre_analysis_targets:
                    self.__write_pre_analysis_targets(mfile, action,
                                                      pre_all_target)
                    post_pre_all_target = 'post_' + pre_all_target

                self.__write_analysis_targets(mfile, action,
                                              post_pre_all_target)

            # Write targets which will be run after pre-analysis phases
            # to post-process the results.
            if self.__pre_analysis:
                self.__write_post_pre_analysis_targets(mfile,
                                                       pre_all_target)
        LOG.info("Done.")
